/**
 * jBox is a jQuery plugin that makes it easy to create customizable tooltips, modal windows, image galleries and more.
 *
 * Author: Stephan Wagner (https://stephanwagner.me)
 *
 * License: MIT (https://opensource.org/licenses/MIT)
 *
 * Requires: jQuery 3.2.1 (https://code.jquery.com/jquery-3.2.1.min.js)
 *
 * Documentation: https://stephanwagner.me/jBox/documentation
 *
 * Demos: https://stephanwagner.me/jBox/demos
 */


// AMD and CommonJS support: http://ifandelse.com/its-not-hard-making-your-library-support-amd-and-commonjs

(function (root, factory) {

    // AMD support

    if (typeof define === 'function' && define.amd) {

        define(['jquery'], function (jQuery) {
            return (root.jBox = factory(jQuery));
        });

        // CommonJS support

    } else if (typeof module === 'object' && module.exports) {

        module.exports = (root.jBox = factory(require('jquery')));

        // Browser

    } else {

        root.jBox = factory(root.jQuery);

    }

}(this, function (jQuery) {


    // The actual jBox plugin starts here

    var jBox = function jBox(type, options) {


        // Options (https://stephanwagner.me/jBox/options)

        this.options = {

            // jBox ID
            id: null,                    // Choose a unique id, otherwise jBox will set one for you (jBox1, jBox2, ...)

            // Dimensions
            width: 'auto',               // The width of the content area, e.g. 'auto', 200, '80%'
            height: 'auto',              // The height of the content area
            minWidth: null,              // Minimal width
            minHeight: null,             // Minimal height
            maxWidth: null,              // Maximal width
            maxHeight: null,             // Maximal height

            // Responsive dimensions
            responsiveWidth: true,       // Adjusts the width to fit the viewport
            responsiveHeight: true,      // Adjusts the height to fit the viewport
            responsiveMinWidth: 100,     // Don't adjust width below this value (in pixel)
            responsiveMinHeight: 100,    // Don't adjust height below this value (in pixel)

            // Attach
            attach: null,                // A jQuery selector to elements that will open and close your jBox, e.g. '.tooltip'
            trigger: 'click',            // The event to open or close your jBox, use 'click', 'touchclick' or 'mouseenter'
            preventDefault: false,       // Prevent the default event when opening jBox, e.g. don't follow the href in a link

            // Content
            content: null,               // You can use HTML or a jQuery element, e.g. jQuery('#jBox-content'). The elements will be appended to the content element and then made visible, so hide them with style="display: none" beforehand
            getContent: null,            // Get the content from an attribute when jBox opens, e.g. getContent: 'data-content'. Use 'html' to get the attached elements HTML as content
            title: null,                 // Adds a title to your jBox
            getTitle: null,              // Get the title from an attribute when jBox opens, e.g. getTitle: 'data-title'
            footer: null,                // Adds a footer to your jBox
            isolateScroll: true,         // Isolates scrolling to the content container

            // AJAX
            ajax: {                      // Setting an URL will make an AJAX request when jBox opens. Optional you can add any jQuery AJAX option (http://api.jquery.com/jquery.ajax/)
                url: null,                 // The URL to send the AJAX request to
                data: '',                  // Data to send with your AJAX request, e.g. {id: 82, limit: 10}
                reload: false,             // Resend the AJAX request when jBox opens. Use true to send the AJAX request only once for every attached element or 'strict' to resend every time jBox opens
                getURL: 'data-url',        // The attribute in the source element where the AJAX request will look for the URL, e.g. data-url="https://ajaxresponse.com"
                getData: 'data-ajax',      // The attribute in the source element where the AJAX request will look for the data, e.g. data-ajax="id=82&limit=10"
                setContent: true,          // Automatically set the response as new content when the AJAX request is finished
                spinner: true,             // Hides the current content and adds a spinner while loading. You can pass HTML content to add your own spinner, e.g. spinner: '<div class="mySpinner"></div>'
                spinnerDelay: 300,         // Milliseconds to wait until spinner appears
                spinnerReposition: true    // Repositions jBox when the spinner is added or removed
            },

            // Position
            target: null,                // The jQuery selector to the target element where jBox will be opened. If no element is found, jBox will use the attached element as target
            position: {
                x: 'center',               // Horizontal position, use a number, 'left', 'right' or 'center'
                y: 'center'                // Vertical position, use a number, 'top', 'bottom' or 'center'
            },
            outside: null,               // Use 'x', 'y', or 'xy' to move your jBox outside of the target element
            offset: 0,                   // Offset to final position, you can set different values for x and y with an object, e.g. {x: 20, y: 10}
            attributes: {                // Note that attributes can only be 'left' or 'right' when using numbers for position, e.g. {x: 300, y: 20}
                x: 'left',                 // Horizontal position, use 'left' or 'right'
                y: 'top'                   // Vertical position, use 'top' or 'bottom'
            },
            fixed: false,                // Your jBox will stay on position when scrolling
            adjustPosition: true,        // Adjusts your jBoxes position if there is not enough space, use 'flip', 'move' or true for both. This option overrides the reposition options
            adjustTracker: false,        // By default jBox adjusts its position when it opens or when the window size changes, set to true to also adjust when scrolling
            adjustDistance: 5,           // The minimal distance to the viewport edge while adjusting. Use an object to set different values, e.g. {top: 50, right: 5, bottom: 20, left: 5}
            reposition: true,            // Calculates new position when the window-size changes
            repositionOnOpen: true,      // Calculates new position each time jBox opens (rather than only when it opens the first time)
            repositionOnContent: true,   // Calculates new position when the content changes with .setContent() or .setTitle()

            // Pointer
            pointer: false,              // Your pointer will always point towards the target element, so the option outside needs to be 'x' or 'y'. By default the pointer is centered, set a position to move it to any side. You can also add an offset, e.g. 'left:30' or 'center:-20'
            pointTo: 'target',           // Setting something else than 'target' will add a pointer even if there is no target element set or found. Use 'top', 'right', 'bottom' or 'left'

            // Animations
            fade: 180,                   // Fade duration in ms, set to 0 or false to disable
            animation: null,             // Animation when opening or closing, use 'pulse', 'zoomIn', 'zoomOut', 'move', 'slide', 'flip', 'tada' (CSS inspired from Daniel Edens Animate.css: http://daneden.me/animate)

            // Appearance
            theme: 'Default',            // Set a jBox theme class
            addClass: null,              // Adds classes to the wrapper
            overlay: false,              // Adds an overlay to hide page content when jBox opens (adjust color and opacity with CSS)
            zIndex: 10000,               // Use a high z-index

            // Delays
            delayOpen: 0,                // Delay opening in ms. Note that the delay will be ignored if your jBox didn't finish closing
            delayClose: 0,               // Delay closing in ms. Nnote that there is always a closing delay of at least 10ms to ensure jBox won't be closed when opening right away

            // Closing
            closeOnEsc: false,           // Close jBox when pressing [esc] key
            closeOnClick: false,         // Close jBox with mouseclick. Use true (click anywhere), 'box' (click on jBox itself), 'overlay' (click on the overlay), 'body' (click anywhere but jBox)
            closeOnMouseleave: false,    // Close jBox when the mouse leaves the jBox area or the area of the attached element
            closeButton: false,          // Adds a close button to your jBox. Use 'title', 'box', 'overlay' or true (true will add the button to the overlay, title or the jBox itself, in that order if any of those elements can be found)

            // Other options
            appendTo: jQuery('body'),    // The element your jBox will be appended to. Any other element than jQuery('body') is only useful for fixed positions or when position values are numbers
            createOnInit: false,         // Creates jBox and makes it available in DOM when it's being initialized, otherwise it will be created when it opens for the first time
            blockScroll: false,          // Blocks scrolling when jBox is open
            draggable: false,            // Make your jBox draggable (use 'true', 'title' or provide an element as handle) (inspired from Chris Coyiers CSS-Tricks http://css-tricks.com/snippets/jquery/draggable-without-jquery-ui/)
            dragOver: true,              // When you have multiple draggable jBoxes, the one you select will always move over the other ones
            autoClose: false,            // Time in ms when jBox will close automatically after it was opened

            // Audio                     // You can use the integrated audio function whenever you'd like to play an audio file, e.g. onInit: function () { this.audio('url_to_audio_file_without_file_extension', 75); }
            preloadAudio: true,          // Preloads the audio files set in option audio. You can also preload other audio files, e.g. ['src_to_file.mp3', 'src_to_file.ogg']
            audio: null,                 // The URL to an audio file to play when jBox opens. Set the URL without file extension, jBox will look for an .mp3 and .ogg file. To play audio when jBox closes, use an object, e.g. {open: 'src_to_audio1', close: 'src_to_audio2'}
            volume: 100,                 // The volume in percent. To have different volumes for opening and closeing, use an object, e.g. {open: 75, close: 100}

            // Events                    // Note that you can use 'this' in all event functions, it refers to your jBox object (e.g. onInit: function () { this.open(); })
            onInit: null,                // Fired when jBox is initialized
            onAttach: null,              // Fired when jBox attached itself to elements, the attached element will be passed as a parameter, e.g. onAttach: function (element) { element.css({color: 'red'}); }
            onPosition: null,            // Fired when jBox is positioned
            onCreated: null,             // Fired when jBox is created and availible in DOM
            onOpen: null,                // Fired when jBox opens
            onClose: null,               // Fired when jBox closes
            onCloseComplete: null        // Fired when jBox is completely closed (when fading is finished)
        };


        // Default plugin options

        this._pluginOptions = {

            // Default options for tooltips
            'Tooltip': {
                getContent: 'title',
                trigger: 'mouseenter',
                position: {
                    x: 'center',
                    y: 'top'
                },
                outside: 'y',
                pointer: true
            },

            // Default options for mouse tooltips
            'Mouse': {
                responsiveWidth: false,
                responsiveHeight: false,
                adjustPosition: 'flip',
                target: 'mouse',
                trigger: 'mouseenter',
                position: {
                    x: 'right',
                    y: 'bottom'
                },
                outside: 'xy',
                offset: 5
            },

            // Default options for modal windows
            'Modal': {
                target: jQuery(window),
                fixed: true,
                blockScroll: true,
                closeOnEsc: true,
                closeOnClick: 'overlay',
                closeButton: true,
                overlay: true,
                animation: 'zoomIn'
            },
        };


        // Merge options

        this.options = jQuery.extend(true, this.options, this._pluginOptions[type] ? this._pluginOptions[type] : jBox._pluginOptions[type], options);


        // Set the jBox type

        jQuery.type(type) == 'string' && (this.type = type);


        // Local function to fire events

        this._fireEvent = function (event, pass) {
            this.options['_' + event] && (this.options['_' + event].bind(this))(pass);
            this.options[event] && (this.options[event].bind(this))(pass);
        };


        // Get a unique jBox ID

        this.options.id === null && (this.options.id = 'jBox' + jBox._getUniqueID());
        this.id = this.options.id;


        // Correct impossible options

        ((this.options.position.x == 'center' && this.options.outside == 'x') || (this.options.position.y == 'center' && this.options.outside == 'y')) && (this.options.outside = null);
        this.options.pointTo == 'target' && (!this.options.outside || this.options.outside == 'xy') && (this.options.pointer = false);


        // Correct multiple choice options

        jQuery.type(this.options.offset) != 'object' ? (this.options.offset = {
            x: this.options.offset,
            y: this.options.offset
        }) : (this.options.offset = jQuery.extend({x: 0, y: 0}, this.options.offset));
        jQuery.type(this.options.adjustDistance) != 'object' ? (this.options.adjustDistance = {
            top: this.options.adjustDistance,
            right: this.options.adjustDistance,
            bottom: this.options.adjustDistance,
            left: this.options.adjustDistance
        }) : (this.options.adjustDistance = jQuery.extend({
            top: 5,
            left: 5,
            right: 5,
            bottom: 5
        }, this.options.adjustDistance));


        // Save default outside position

        this.outside = this.options.outside && this.options.outside != 'xy' ? this.options.position[this.options.outside] : false;


        // Save where the jBox is aligned to

        this.align = this.outside ? this.outside : (this.options.position.y != 'center' && jQuery.type(this.options.position.y) != 'number' ? this.options.position.x : (this.options.position.x != 'center' && jQuery.type(this.options.position.x) != 'number' ? this.options.position.y : this.options.attributes.x));


        // Internal positioning functions

        this._getOpp = function (opp) {
            return {left: 'right', right: 'left', top: 'bottom', bottom: 'top', x: 'y', y: 'x'}[opp];
        };
        this._getXY = function (xy) {
            return {left: 'x', right: 'x', top: 'y', bottom: 'y', center: 'x'}[xy];
        };
        this._getTL = function (tl) {
            return {left: 'left', right: 'left', top: 'top', bottom: 'top', center: 'left', x: 'left', y: 'top'}[tl];
        };


        // Get a dimension value in integer pixel dependent on appended element

        this._getInt = function (value, dimension) {
            if (value == 'auto') return 'auto';
            if (value && jQuery.type(value) == 'string' && value.slice(-1) == '%') {
                return jQuery(window)[dimension == 'height' ? 'innerHeight' : 'innerWidth']() * parseInt(value.replace('%', '')) / 100;
            }
            return value;
        };


        // Create an svg element

        this._createSVG = function (type, options) {
            var svg = document.createElementNS('http://www.w3.org/2000/svg', type);
            jQuery.each(options, function (index, item) {
                svg.setAttribute(item[0], (item[1] || ''));
            });
            return svg;
        };


        // Isolate scrolling in a container

        this._isolateScroll = function (el) {
            // Abort if element not found
            if (!el || !el.length) return;

            el.on('DOMMouseScroll.jBoxIsolateScroll mousewheel.jBoxIsolateScroll', function (ev) {
                var delta = ev.wheelDelta || (ev.originalEvent && ev.originalEvent.wheelDelta) || -ev.detail;
                var overflowBottom = this.scrollTop + el.outerHeight() - this.scrollHeight >= 0;
                var overflowTop = this.scrollTop <= 0;
                ((delta < 0 && overflowBottom) || (delta > 0 && overflowTop)) && ev.preventDefault();
            });
        };


        // Set the title width to content width 

        this._setTitleWidth = function () {
            // Abort if there is no title or width of content is auto
            if (!this.titleContainer || (this.content[0].style.width == 'auto' && !this.content[0].style.maxWidth)) return null;

            // Expose wrapper to get actual width
            if (this.wrapper.css('display') == 'none') {
                this.wrapper.css('display', 'block');
                var contentWidth = this.content.outerWidth();
                this.wrapper.css('display', 'none');
            } else {
                var contentWidth = this.content.outerWidth();
            }

            // Set max-width only
            this.titleContainer.css({maxWidth: (Math.max(contentWidth, parseInt(this.content[0].style.maxWidth)) || null)});
        }


        // Make jBox draggable

        this._draggable = function () {
            // Abort if jBox is not draggable
            if (!this.options.draggable) return false;

            // Get the handle where jBox will be dragged with
            var handle = this.options.draggable == 'title' ? this.titleContainer : (this.options.draggable instanceof jQuery ? this.options.draggable : (jQuery.type(this.options.draggable) == 'string' ? jQuery(this.options.draggable) : this.wrapper));

            // Abort if no handle or if draggable was set already
            if (!handle || !(handle instanceof jQuery) || !handle.length || handle.data('jBox-draggable')) return false;

            // Add mouse events
            handle.addClass('jBox-draggable').data('jBox-draggable', true).on('mousedown', function (ev) {
                if (ev.button == 2 || jQuery(ev.target).hasClass('jBox-noDrag') || jQuery(ev.target).parents('.jBox-noDrag').length) return;

                // Adjust z-index when dragging jBox over another draggable jBox
                if (this.options.dragOver && this.wrapper.css('zIndex') <= jBox.zIndexMax) {
                    jBox.zIndexMax += 1;
                    this.wrapper.css('zIndex', jBox.zIndexMax);
                }

                var drg_h = this.wrapper.outerHeight();
                var drg_w = this.wrapper.outerWidth();
                var pos_y = this.wrapper.offset().top + drg_h - ev.pageY;
                var pos_x = this.wrapper.offset().left + drg_w - ev.pageX;

                jQuery(document).on('mousemove.jBox-draggable-' + this.id, function (ev) {
                    this.wrapper.offset({
                        top: ev.pageY + pos_y - drg_h,
                        left: ev.pageX + pos_x - drg_w
                    });
                }.bind(this));
                ev.preventDefault();

            }.bind(this)).on('mouseup', function () {
                jQuery(document).off('mousemove.jBox-draggable-' + this.id);
            }.bind(this));

            // Get highest z-index
            jBox.zIndexMax = !jBox.zIndexMax ? this.options.zIndex : Math.max(jBox.zIndexMax, this.options.zIndex);


            return this;
        };

        // Create jBox

        this._create = function () {
            // Abort if jBox was created already
            if (this.wrapper) return;

            // Create wrapper
            this.wrapper = jQuery('<div/>', {
                id: this.id,
                'class': 'jBox-wrapper' + (this.type ? ' jBox-' + this.type : '') + (this.options.theme ? ' jBox-' + this.options.theme : '') + (this.options.addClass ? ' ' + this.options.addClass : '')
            }).css({
                position: (this.options.fixed ? 'fixed' : 'absolute'),
                display: 'none',
                opacity: 0,
                zIndex: this.options.zIndex

                // Save the jBox instance in the wrapper, so you can get access to your jBox when you only have the element
            }).data('jBox', this);

            // Add mouseleave event, only close jBox when the new target is not the source element
            this.options.closeOnMouseleave && this.wrapper.on('mouseleave', function (ev) {
                !this.source || !(ev.relatedTarget == this.source[0] || jQuery.inArray(this.source[0], jQuery(ev.relatedTarget).parents('*')) !== -1) && this.close();
            }.bind(this));

            // Add closeOnClick: 'box' events
            (this.options.closeOnClick == 'box') && this.wrapper.on('touchend click', function () {
                this.close({ignoreDelay: true});
            }.bind(this));

            // Create container
            this.container = jQuery('<div class="jBox-container"/>').appendTo(this.wrapper);

            // Create content
            this.content = jQuery('<div class="jBox-content"/>').appendTo(this.container);

            // Create footer
            this.options.footer && (this.footer = jQuery('<div class="jBox-footer"/>').append(this.options.footer).appendTo(this.container));

            // Isolate scrolling
            this.options.isolateScroll && this._isolateScroll(this.content);

            // Create close button
            if (this.options.closeButton) {
                var closeButtonSVG = this._createSVG('svg', [['viewBox', '0 0 24 24']]);
                closeButtonSVG.appendChild(this._createSVG('path', [['d', 'M22.2,4c0,0,0.5,0.6,0,1.1l-6.8,6.8l6.9,6.9c0.5,0.5,0,1.1,0,1.1L20,22.3c0,0-0.6,0.5-1.1,0L12,15.4l-6.9,6.9c-0.5,0.5-1.1,0-1.1,0L1.7,20c0,0-0.5-0.6,0-1.1L8.6,12L1.7,5.1C1.2,4.6,1.7,4,1.7,4L4,1.7c0,0,0.6-0.5,1.1,0L12,8.5l6.8-6.8c0.5-0.5,1.1,0,1.1,0L22.2,4z']]));
                this.closeButton = jQuery('<div class="jBox-closeButton jBox-noDrag"/>').on('touchend click', function (ev) {
                    this.close({ignoreDelay: true});
                }.bind(this)).append(closeButtonSVG);

                // Add close button to jBox container
                if (this.options.closeButton == 'box' || (this.options.closeButton === true && !this.options.overlay && !this.options.title)) {
                    this.wrapper.addClass('jBox-closeButton-box');
                    this.closeButton.appendTo(this.container);
                }
            }

            // Append jBox to DOM
            this.wrapper.appendTo(this.options.appendTo);

            // Fix adjustDistance if there is a close button in the box
            this.wrapper.find('.jBox-closeButton').length && jQuery.each(['top', 'right', 'bottom', 'left'], function (index, pos) {
                this.wrapper.find('.jBox-closeButton').css(pos) && this.wrapper.find('.jBox-closeButton').css(pos) != 'auto' && (this.options.adjustDistance[pos] = Math.max(this.options.adjustDistance[pos], this.options.adjustDistance[pos] + (((parseInt(this.wrapper.find('.jBox-closeButton').css(pos)) || 0) + (parseInt(this.container.css('border-' + pos + '-width')) || 0)) * -1)));
            }.bind(this));

            // Create pointer
            if (this.options.pointer) {

                // Get pointer vars and save globally
                this.pointer = {
                    position: (this.options.pointTo != 'target') ? this.options.pointTo : this._getOpp(this.outside),
                    xy: (this.options.pointTo != 'target') ? this._getXY(this.options.pointTo) : this._getXY(this.outside),
                    align: 'center',
                    offset: 0
                };

                this.pointer.element = jQuery('<div class="jBox-pointer jBox-pointer-' + this.pointer.position + '"/>').appendTo(this.wrapper);
                this.pointer.dimensions = {
                    x: this.pointer.element.outerWidth(),
                    y: this.pointer.element.outerHeight()
                };

                if (jQuery.type(this.options.pointer) == 'string') {
                    var split = this.options.pointer.split(':');
                    split[0] && (this.pointer.align = split[0]);
                    split[1] && (this.pointer.offset = parseInt(split[1]));
                }
                this.pointer.alignAttribute = (this.pointer.xy == 'x' ? (this.pointer.align == 'bottom' ? 'bottom' : 'top') : (this.pointer.align == 'right' ? 'right' : 'left'));

                // Set wrapper CSS
                this.wrapper.css('padding-' + this.pointer.position, this.pointer.dimensions[this.pointer.xy]);

                // Set pointer CSS
                this.pointer.element.css(this.pointer.alignAttribute, (this.pointer.align == 'center' ? '50%' : 0)).css('margin-' + this.pointer.alignAttribute, this.pointer.offset);
                this.pointer.margin = {};
                this.pointer.margin['margin-' + this.pointer.alignAttribute] = this.pointer.offset;

                // Add a transform to fix centered position
                (this.pointer.align == 'center') && this.pointer.element.css('transform', 'translate(' + (this.pointer.xy == 'y' ? (this.pointer.dimensions.x * -0.5 + 'px') : 0) + ', ' + (this.pointer.xy == 'x' ? (this.pointer.dimensions.y * -0.5 + 'px') : 0) + ')');

                this.pointer.element.css((this.pointer.xy == 'x' ? 'width' : 'height'), parseInt(this.pointer.dimensions[this.pointer.xy]) + parseInt(this.container.css('border-' + this.pointer.alignAttribute + '-width')));

                // Add class to wrapper for CSS access
                this.wrapper.addClass('jBox-pointerPosition-' + this.pointer.position);
            }

            // Set title and content
            this.setContent(this.options.content, true);
            this.setTitle(this.options.title, true);

            this.options.draggable && this._draggable();

            // Fire onCreated event
            this._fireEvent('onCreated');
        };


        // Create jBox onInit

        this.options.createOnInit && this._create();


        // Attach jBox

        this.options.attach && this.attach();


        // Attach document and window events

        this._attachEvents = function () {
            // Closing event: closeOnEsc
            this.options.closeOnEsc && jQuery(document).on('keyup.jBox-' + this.id, function (ev) {
                if (ev.keyCode == 27) {
                    this.close({ignoreDelay: true});
                }
            }.bind(this));

            // Closing event: closeOnClick
            if (this.options.closeOnClick === true || this.options.closeOnClick == 'body') {
                jQuery(document).on('touchend.jBox-' + this.id + ' click.jBox-' + this.id, function (ev) {
                    if (this.blockBodyClick || (this.options.closeOnClick == 'body' && (ev.target == this.wrapper[0] || this.wrapper.has(ev.target).length))) return;
                    this.close({ignoreDelay: true});
                }.bind(this));
            }

            // Positioning events
            if ((this.options.adjustPosition || this.options.reposition) && !this.fixed && this.outside) {

                // Trigger position events when scrolling
                this.options.adjustTracker && jQuery(window).on('scroll.jBox-' + this.id, function (ev) {
                    this.position();
                }.bind(this));

                // Trigger position events when resizing
                (this.options.adjustPosition || this.options.reposition) && jQuery(window).on('resize.jBox-' + this.id, function (ev) {
                    this.position();
                }.bind(this));
            }

            // Mousemove events
            this.options.target == 'mouse' && jQuery('body').on('mousemove.jBox-' + this.id, function (ev) {
                this.position({mouseTarget: {top: ev.pageY, left: ev.pageX}});
            }.bind(this));
        };


        // Detach document and window events

        this._detachEvents = function () {
            // Closing event: closeOnEsc
            this.options.closeOnEsc && jQuery(document).off('keyup.jBox-' + this.id);

            // Closing event: closeOnClick
            (this.options.closeOnClick === true || this.options.closeOnClick == 'body') && jQuery(document).off('touchend.jBox-' + this.id + ' click.jBox-' + this.id);

            // Positioning events
            this.options.adjustTracker && jQuery(window).off('scroll.jBox-' + this.id);
            (this.options.adjustPosition || this.options.reposition) && jQuery(window).off('resize.jBox-' + this.id);

            // Mousemove events
            this.options.target == 'mouse' && jQuery('body').off('mousemove.jBox-' + this.id);
        };


        // Show overlay

        this._showOverlay = function () {
            // Create the overlay if wasn't created already
            if (!this.overlay) {

                // Create element and append to the element where jBox is appended to
                this.overlay = jQuery('<div id="' + this.id + '-overlay"/>').addClass('jBox-overlay' + (this.type ? ' jBox-overlay-' + this.type : '')).css({
                    display: 'none',
                    opacity: 0,
                    zIndex: this.options.zIndex - 1
                }).appendTo(this.options.appendTo);

                // Add close button to overlay
                (this.options.closeButton == 'overlay' || (this.options.closeButton === true && !this.titleContainer)) && this.overlay.append(this.closeButton);

                // Add closeOnClick: 'overlay' events
                this.options.closeOnClick == 'overlay' && this.overlay.on('touchend click', function () {
                    this.close({ignoreDelay: true});
                }.bind(this));

                // Adjust option adjustDistance if there is a close button in the overlay
                jQuery('#' + this.id + '-overlay .jBox-closeButton').length && (this.options.adjustDistance.top = Math.max(jQuery('#' + this.id + '-overlay .jBox-closeButton').outerHeight(), this.options.adjustDistance.top));
            }

            // Abort if overlay is already visible
            if (this.overlay.css('display') == 'block') return;

            // Show overlay
            this.options.fade ? (this.overlay.stop() && this.overlay.animate({opacity: 1}, {
                queue: false,
                duration: this.options.fade,
                start: function () {
                    this.overlay.css({display: 'block'});
                }.bind(this)
            })) : this.overlay.css({display: 'block', opacity: 1});
        };


        // Hide overlay

        this._hideOverlay = function () {
            // Abort if the overlay wasn't created yet
            if (!this.overlay) return;

            // Hide overlay if no other jBox needs it
            this.options.fade ? (this.overlay.stop() && this.overlay.animate({opacity: 0}, {
                queue: false,
                duration: this.options.fade,
                complete: function () {
                    this.overlay.css({display: 'none'});
                }.bind(this)
            })) : this.overlay.css({display: 'none', opacity: 0});
        };


        // Get the correct jBox dimensions by moving jBox out of viewport

        this._exposeDimensions = function () {
            // Move wrapper out of viewport
            this.wrapper.css({
                top: -10000,
                left: -10000,
                right: 'auto',
                bottom: 'auto'
            });

            // Get jBox dimensions
            var jBoxDimensions = {
                x: this.wrapper.outerWidth(),
                y: this.wrapper.outerHeight()
            };

            // Reset position to viewport
            this.wrapper.css({
                top: 'auto',
                left: 'auto'
            });

            return jBoxDimensions;
        };


        // Generate CSS for animations and append to header

        this._generateAnimationCSS = function () {
            // Get open and close animations if none provided
            (jQuery.type(this.options.animation) != 'object') && (this.options.animation = {
                pulse: {open: 'pulse', close: 'zoomOut'},
                zoomIn: {open: 'zoomIn', close: 'zoomIn'},
                zoomOut: {open: 'zoomOut', close: 'zoomOut'},
                move: {open: 'move', close: 'move'},
                slide: {open: 'slide', close: 'slide'},
                flip: {open: 'flip', close: 'flip'},
                tada: {open: 'tada', close: 'zoomOut'}
            }[this.options.animation]);

            // Abort if animation not found
            if (!this.options.animation) return null;

            // Get direction var
            this.options.animation.open && (this.options.animation.open = this.options.animation.open.split(':'));
            this.options.animation.close && (this.options.animation.close = this.options.animation.close.split(':'));
            this.options.animation.openDirection = this.options.animation.open[1] ? this.options.animation.open[1] : null;
            this.options.animation.closeDirection = this.options.animation.close[1] ? this.options.animation.close[1] : null;
            this.options.animation.open && (this.options.animation.open = this.options.animation.open[0]);
            this.options.animation.close && (this.options.animation.close = this.options.animation.close[0]);

            // Add 'Open' and 'Close' to animation names
            this.options.animation.open && (this.options.animation.open += 'Open');
            this.options.animation.close && (this.options.animation.close += 'Close');

            // All animations
            var animations = {
                pulse: {
                    duration: 350,
                    css: [['0%', 'scale(1)'], ['50%', 'scale(1.1)'], ['100%', 'scale(1)']]
                },
                zoomInOpen: {
                    duration: (this.options.fade || 180),
                    css: [['0%', 'scale(0.9)'], ['100%', 'scale(1)']]
                },
                zoomInClose: {
                    duration: (this.options.fade || 180),
                    css: [['0%', 'scale(1)'], ['100%', 'scale(0.9)']]
                },
                zoomOutOpen: {
                    duration: (this.options.fade || 180),
                    css: [['0%', 'scale(1.1)'], ['100%', 'scale(1)']]
                },
                zoomOutClose: {
                    duration: (this.options.fade || 180),
                    css: [['0%', 'scale(1)'], ['100%', 'scale(1.1)']]
                },
                moveOpen: {
                    duration: (this.options.fade || 180),
                    positions: {top: {'0%': -12}, right: {'0%': 12}, bottom: {'0%': 12}, left: {'0%': -12}},
                    css: [['0%', 'translate%XY(%Vpx)'], ['100%', 'translate%XY(0px)']]
                },
                moveClose: {
                    duration: (this.options.fade || 180),
                    timing: 'ease-in',
                    positions: {top: {'100%': -12}, right: {'100%': 12}, bottom: {'100%': 12}, left: {'100%': -12}},
                    css: [['0%', 'translate%XY(0px)'], ['100%', 'translate%XY(%Vpx)']]
                },
                slideOpen: {
                    duration: 400,
                    positions: {top: {'0%': -400}, right: {'0%': 400}, bottom: {'0%': 400}, left: {'0%': -400}},
                    css: [['0%', 'translate%XY(%Vpx)'], ['100%', 'translate%XY(0px)']]
                },
                slideClose: {
                    duration: 400,
                    timing: 'ease-in',
                    positions: {top: {'100%': -400}, right: {'100%': 400}, bottom: {'100%': 400}, left: {'100%': -400}},
                    css: [['0%', 'translate%XY(0px)'], ['100%', 'translate%XY(%Vpx)']]
                },
                flipOpen: {
                    duration: 600,
                    css: [['0%', 'perspective(400px) rotateX(90deg)'], ['40%', 'perspective(400px) rotateX(-15deg)'], ['70%', 'perspective(400px) rotateX(15deg)'], ['100%', 'perspective(400px) rotateX(0deg)']]
                },
                flipClose: {
                    duration: (this.options.fade || 300),
                    css: [['0%', 'perspective(400px) rotateX(0deg)'], ['100%', 'perspective(400px) rotateX(90deg)']]
                },
                tada: {
                    duration: 800,
                    css: [['0%', 'scale(1)'], ['10%, 20%', 'scale(0.9) rotate(-3deg)'], ['30%, 50%, 70%, 90%', 'scale(1.1) rotate(3deg)'], ['40%, 60%, 80%', 'scale(1.1) rotate(-3deg)'], ['100%', 'scale(1) rotate(0)']]
                }
            };

            // Set Open and Close names for standalone animations
            jQuery.each(['pulse', 'tada'], function (index, item) {
                animations[item + 'Open'] = animations[item + 'Close'] = animations[item];
            });

            // Function to generate the CSS for the keyframes
            var generateKeyframeCSS = function (ev, position) {
                // Generate keyframes CSS
                keyframe_css = '@keyframes jBox-' + this.id + '-animation-' + this.options.animation[ev] + '-' + ev + (position ? '-' + position : '') + ' {';
                jQuery.each(animations[this.options.animation[ev]].css, function (index, item) {
                    var translate = position ? item[1].replace('%XY', this._getXY(position).toUpperCase()) : item[1];
                    animations[this.options.animation[ev]].positions && (translate = translate.replace('%V', animations[this.options.animation[ev]].positions[position][item[0]]));
                    keyframe_css += item[0] + ' {transform:' + translate + ';}';
                }.bind(this));
                keyframe_css += '}';

                // Generate class CSS
                keyframe_css += '.jBox-' + this.id + '-animation-' + this.options.animation[ev] + '-' + ev + (position ? '-' + position : '') + ' {';
                keyframe_css += 'animation-duration: ' + animations[this.options.animation[ev]].duration + 'ms;';
                keyframe_css += 'animation-name: jBox-' + this.id + '-animation-' + this.options.animation[ev] + '-' + ev + (position ? '-' + position : '') + ';';
                keyframe_css += animations[this.options.animation[ev]].timing ? ('animation-timing-function: ' + animations[this.options.animation[ev]].timing + ';') : '';
                keyframe_css += '}';

                return keyframe_css;
            }.bind(this);

            // Generate css for each event and positions
            this._animationCSS = '';
            jQuery.each(['open', 'close'], function (index, ev) {
                // No CSS needed for closing with no fade
                if (!this.options.animation[ev] || !animations[this.options.animation[ev]] || (ev == 'close' && !this.options.fade)) return '';

                // Generate CSS
                animations[this.options.animation[ev]].positions ?
                    jQuery.each(['top', 'right', 'bottom', 'left'], function (index2, position) {
                        this._animationCSS += generateKeyframeCSS(ev, position);
                    }.bind(this)) :
                    this._animationCSS += generateKeyframeCSS(ev);
            }.bind(this));

        };


        // Add css for animations

        this.options.animation && this._generateAnimationCSS();


        // Block body clicks for 10ms to prevent extra event triggering

        this._blockBodyClick = function () {
            this.blockBodyClick = true;
            setTimeout(function () {
                this.blockBodyClick = false;
            }.bind(this), 10);
        };


        // Animations

        this._animate = function (ev) {
            // The event which triggers the animation
            !ev && (ev = this.isOpen ? 'open' : 'close');

            // Don't animate when closing with no fade duration
            if (!this.options.fade && ev == 'close') return null;

            // Get the current position, use opposite if jBox is flipped
            var animationDirection = (this.options.animation[ev + 'Direction'] || ((this.align != 'center') ? this.align : this.options.attributes.x));
            this.flipped && this._getXY(animationDirection) == (this._getXY(this.align)) && (animationDirection = this._getOpp(animationDirection));

            // Add event and position classes
            var classnames = 'jBox-' + this.id + '-animation-' + this.options.animation[ev] + '-' + ev + ' jBox-' + this.id + '-animation-' + this.options.animation[ev] + '-' + ev + '-' + animationDirection;
            this.wrapper.addClass(classnames);

            // Get duration of animation
            var animationDuration = parseFloat(this.wrapper.css('animation-duration')) * 1000;
            ev == 'close' && (animationDuration = Math.min(animationDuration, this.options.fade));

            // Remove animation classes when animation is finished
            setTimeout(function () {
                this.wrapper.removeClass(classnames);
            }.bind(this), animationDuration);
        };


        // Abort an animation

        this._abortAnimation = function () {
            // Remove all animation classes
            var classes = this.wrapper.attr('class').split(' ').filter(function (c) {
                return c.lastIndexOf('jBox-' + this.id + '-animation', 0) !== 0;
            }.bind(this));
            this.wrapper.attr('class', classes.join(' '));
        };


        // Adjust dimensions when browser is resized

        if (this.options.responsiveWidth || this.options.responsiveHeight) {
            // Responsive positioning overrides options adjustPosition and reposition
            // TODO: Only add this resize event when the other one from adjustPosition and reposition was not set
            jQuery(window).on('resize.responsivejBox-' + this.id, function (ev) {
                if (this.isOpen) {
                    this.position();
                }
            }.bind(this));
        }


        // Fix audio options

        jQuery.type(this.options.preloadAudio) === 'string' && (this.options.preloadAudio = [this.options.preloadAudio]);
        jQuery.type(this.options.audio) === 'string' && (this.options.audio = {open: this.options.audio});
        jQuery.type(this.options.volume) === 'number' && (this.options.volume = {
            open: this.options.volume,
            close: this.options.volume
        });

        if (this.options.preloadAudio === true && this.options.audio) {
            this.options.preloadAudio = [];
            jQuery.each(this.options.audio, function (index, url) {
                this.options.preloadAudio.push(url + '.mp3');
                this.options.preloadAudio.push(url + '.ogg');
            }.bind(this));
        }


        // Preload audio files

        this.options.preloadAudio.length && jQuery.each(this.options.preloadAudio, function (index, url) {
            var audio = new Audio();
            audio.src = url;
            audio.preload = 'auto';
        });


        // Fire onInit event

        this._fireEvent('onInit');


        return this;
    };


    // Attach jBox to elements

    jBox.prototype.attach = function (elements, trigger) {
        // Get elements from options if none passed
        !elements && (elements = this.options.attach);

        // Convert selectors to jQuery objects
        jQuery.type(elements) == 'string' && (elements = jQuery(elements))

        // Get trigger event from options if not passed
        !trigger && (trigger = this.options.trigger);

        // Loop through elements and attach jBox
        elements && elements.length && jQuery.each(elements, function (index, el) {
            el = jQuery(el);

            // Only attach if the element wasn't attached to this jBox already
            if (!el.data('jBox-attached-' + this.id)) {

                // Remove title attribute and store content on element
                (this.options.getContent == 'title' && el.attr('title') != undefined) && el.data('jBox-getContent', el.attr('title')).removeAttr('title');

                // Add Element to collection
                this.attachedElements || (this.attachedElements = []);
                this.attachedElements.push(el[0]);

                // Add click or mouseenter event, click events can prevent default as well
                el.on(trigger + '.jBox-attach-' + this.id, function (ev) {
                    // Clear timer
                    this.timer && clearTimeout(this.timer);

                    // Block opening when jbox is open and the source element is triggering
                    if (trigger == 'mouseenter' && this.isOpen && this.source[0] == el[0]) return;

                    // Only close jBox if you click the current target element, otherwise open at new target
                    if (this.isOpen && this.source && this.source[0] != el[0]) var forceOpen = true;

                    // Set new source element
                    this.source = el;

                    // Set new target
                    !this.options.target && (this.target = el);

                    // Prevent default action on click
                    trigger == 'click' && this.options.preventDefault && ev.preventDefault();

                    // Toggle or open jBox
                    this[trigger == 'click' && !forceOpen ? 'toggle' : 'open']();

                }.bind(this));

                // Add close event for trigger event mouseenter
                (this.options.trigger == 'mouseenter') && el.on('mouseleave', function (ev) {
                    // Abort if jBox wasn't created yet
                    if (!this.wrapper) return null;

                    // If we have set closeOnMouseleave, do not close jBox when leaving attached element and mouse is over jBox
                    if (!this.options.closeOnMouseleave || !(ev.relatedTarget == this.wrapper[0] || jQuery(ev.relatedTarget).parents('#' + this.id).length)) this.close();
                }.bind(this));

                // Store 
                el.data('jBox-attached-' + this.id, trigger);

                // Fire onAttach event
                this._fireEvent('onAttach', el);
            }

        }.bind(this));

        return this;
    };


    // Detach jBox from elements

    jBox.prototype.detach = function (elements) {
        // Get elements from stores elements if none passed
        !elements && (elements = this.attachedElements || []);

        elements && elements.length && jQuery.each(elements, function (index, el) {
            el = jQuery(el);

            // Remove events
            if (el.data('jBox-attached-' + this.id)) {
                el.off(el.data('jBox-attached-' + this.id) + '.jBox-attach-' + this.id);
                el.data('jBox-attached-' + this.id, null);
            }
            // Remove element from collection
            this.attachedElements = jQuery.grep(this.attachedElements, function (value) {
                return value != el[0];
            });
        }.bind(this));

        return this;
    };


    // Set title

    jBox.prototype.setTitle = function (title, ignore_positioning) {
        // Abort if title to set
        if (title == null || title == undefined) return this;

        // Create jBox if it wasn't created already
        !this.wrapper && this._create();

        // Get the width and height of wrapper, only if they change we need to reposition
        var wrapperHeight = this.wrapper.outerHeight();
        var wrapperWidth = this.wrapper.outerWidth();

        // Create title elements if they weren't created already
        if (!this.title) {
            this.titleContainer = jQuery('<div class="jBox-title"/>');
            this.title = jQuery('<div/>').appendTo(this.titleContainer);
            this.wrapper.addClass('jBox-hasTitle');
            if (this.options.closeButton == 'title' || (this.options.closeButton === true && !this.options.overlay)) {
                this.wrapper.addClass('jBox-closeButton-title');
                this.closeButton.appendTo(this.titleContainer);
            }
            this.titleContainer.insertBefore(this.content);
            this._setTitleWidth();
        }
        this.title.html(title);

        // Adjust width of title
        wrapperWidth != this.wrapper.outerWidth() && this._setTitleWidth();

        // Make jBox draggable
        this.options.draggable && this._draggable();

        // Reposition if dimensions changed
        !ignore_positioning && this.options.repositionOnContent && (wrapperHeight != this.wrapper.outerHeight() || wrapperWidth != this.wrapper.outerWidth()) && this.position();

        return this;
    };


    // Set content

    jBox.prototype.setContent = function (content, ignore_positioning) {
        // Abort if no content to set
        if (content == null || content == undefined) return this;

        // Create jBox if it wasn't created already
        !this.wrapper && this._create();

        // Get the width and height of wrapper, only if they change we need to reposition
        var wrapperHeight = this.wrapper.outerHeight();
        var wrapperWidth = this.wrapper.outerWidth();

        // Move all appended containers to body
        this.content.children('[data-jbox-content-appended]').appendTo('body').css({display: 'none'});

        // Set the new content
        switch (jQuery.type(content)) {
            case 'string':
                this.content.html(content);
                break;
            case 'object':
                this.content.html('');
                content.attr('data-jbox-content-appended', 1).appendTo(this.content).css({display: 'block'});
                break;
        }

        // Adjust title width
        wrapperWidth != this.wrapper.outerWidth() && this._setTitleWidth();

        // Make jBox draggable
        this.options.draggable && this._draggable();

        // Reposition if dimensions changed
        !ignore_positioning && this.options.repositionOnContent && (wrapperHeight != this.wrapper.outerHeight() || wrapperWidth != this.wrapper.outerWidth()) && this.position();

        return this;
    };


    // Set jBox dimensions

    jBox.prototype.setDimensions = function (type, value, pos) {
        // Create jBox if it wasn't created already
        !this.wrapper && this._create();

        // Default value is 'auto'
        value == undefined && (value = 'auto');

        // Set CSS of content and title
        this.content.css(type, this._getInt(value));

        // Adjust title width
        type == 'width' && this._setTitleWidth();

        // Reposition by default
        (pos == undefined || pos) && this.position();
    };


    // Set jBox width or height

    jBox.prototype.setWidth = function (value, pos) {
        this.setDimensions('width', value, pos);
    };
    jBox.prototype.setHeight = function (value, pos) {
        this.setDimensions('height', value, pos);
    };


    // Position jBox

    jBox.prototype.position = function (options) {
        // Options are required
        !options && (options = {});

        // Combine passed options with jBox options
        options = jQuery.extend(true, this.options, options);

        // Get the target
        this.target = options.target || this.target || jQuery(window);

        // Make sure target is a jQuery element
        !(this.target instanceof jQuery || this.target == 'mouse') && (this.target = jQuery(this.target));

        // Abort if target is missing
        if (!this.target.length) return this;

        // Reset content css to get original dimensions
        this.content.css({
            width: this._getInt(options.width, 'width'),
            height: this._getInt(options.height, 'height'),
            minWidth: this._getInt(options.minWidth, 'width'),
            minHeight: this._getInt(options.minHeight, 'height'),
            maxWidth: this._getInt(options.maxWidth, 'width'),
            maxHeight: this._getInt(options.maxHeight, 'height'),
        });

        // Reset width of title
        this._setTitleWidth();

        // Get jBox dimensions
        var jBoxDimensions = this._exposeDimensions();

        // Check if target has fixed position, store in elements data
        this.target != 'mouse' && !this.target.data('jBox-' + this.id + '-fixed') && this.target.data('jBox-' + this.id + '-fixed', (this.target[0] != jQuery(window)[0] && (this.target.css('position') == 'fixed' || this.target.parents().filter(function () {
            return jQuery(this).css('position') == 'fixed';
        }).length > 0)) ? 'fixed' : 'static');

        // Get the window dimensions
        var windowDimensions = {
            x: jQuery(window).outerWidth(),
            y: jQuery(window).outerHeight(),
            top: (options.fixed && this.target.data('jBox-' + this.id + '-fixed') ? 0 : jQuery(window).scrollTop()),
            left: (options.fixed && this.target.data('jBox-' + this.id + '-fixed') ? 0 : jQuery(window).scrollLeft())
        };
        windowDimensions.bottom = windowDimensions.top + windowDimensions.y;
        windowDimensions.right = windowDimensions.left + windowDimensions.x;

        // Get target offset
        try {
            var targetOffset = this.target.offset();
        } catch (e) {
            var targetOffset = {top: 0, left: 0};
        }
        ;

        // When the target is fixed and jBox is fixed, remove scroll offset
        if (this.target != 'mouse' && this.target.data('jBox-' + this.id + '-fixed') == 'fixed' && options.fixed) {
            targetOffset.top = targetOffset.top - jQuery(window).scrollTop();
            targetOffset.left = targetOffset.left - jQuery(window).scrollLeft();
        }

        // Get target dimensions
        var targetDimensions = {
            x: this.target == 'mouse' ? 12 : this.target.outerWidth(),
            y: this.target == 'mouse' ? 20 : this.target.outerHeight(),
            top: this.target == 'mouse' && options.mouseTarget ? options.mouseTarget.top : (targetOffset ? targetOffset.top : 0),
            left: this.target == 'mouse' && options.mouseTarget ? options.mouseTarget.left : (targetOffset ? targetOffset.left : 0)
        };

        // Check if jBox is outside
        var outside = options.outside && !(options.position.x == 'center' && options.position.y == 'center');

        // Get the available space on all sides
        var availableSpace = {
            x: windowDimensions.x - options.adjustDistance.left - options.adjustDistance.right, // TODO: substract position.x when they are numbers
            y: windowDimensions.y - options.adjustDistance.top - options.adjustDistance.bottom, // TODO: substract position.x when they are numbers
            left: !outside ? 0 : (targetDimensions.left - jQuery(window).scrollLeft() - options.adjustDistance.left),
            right: !outside ? 0 : (windowDimensions.x - targetDimensions.left + jQuery(window).scrollLeft() - targetDimensions.x - options.adjustDistance.right),
            top: !outside ? 0 : (targetDimensions.top - jQuery(window).scrollTop() - this.options.adjustDistance.top),
            bottom: !outside ? 0 : (windowDimensions.y - targetDimensions.top + jQuery(window).scrollTop() - targetDimensions.y - options.adjustDistance.bottom),
        };

        // Get the default outside position, check if box will be flipped
        var jBoxOutsidePosition = {
            x: (options.outside == 'x' || options.outside == 'xy') && jQuery.type(options.position.x) != 'number' ? options.position.x : null,
            y: (options.outside == 'y' || options.outside == 'xy') && jQuery.type(options.position.y) != 'number' ? options.position.y : null
        };
        var flip = {x: false, y: false};
        (jBoxOutsidePosition.x && jBoxDimensions.x > availableSpace[jBoxOutsidePosition.x] && availableSpace[this._getOpp(jBoxOutsidePosition.x)] > availableSpace[jBoxOutsidePosition.x]) && (jBoxOutsidePosition.x = this._getOpp(jBoxOutsidePosition.x)) && (flip.x = true);
        (jBoxOutsidePosition.y && jBoxDimensions.y > availableSpace[jBoxOutsidePosition.y] && availableSpace[this._getOpp(jBoxOutsidePosition.y)] > availableSpace[jBoxOutsidePosition.y]) && (jBoxOutsidePosition.y = this._getOpp(jBoxOutsidePosition.y)) && (flip.y = true);

        // Adjust responsive dimensions
        if (options.responsiveWidth || options.responsiveHeight) {

            // Adjust width and height according to default outside position
            var adjustResponsiveWidth = function () {
                if (options.responsiveWidth && jBoxDimensions.x > availableSpace[jBoxOutsidePosition.x || 'x']) {
                    var contentWidth = availableSpace[jBoxOutsidePosition.x || 'x'] - (this.pointer && outside && options.outside == 'x' ? this.pointer.dimensions.x : 0) - parseInt(this.container.css('border-left-width')) - parseInt(this.container.css('border-right-width'));
                    this.content.css({
                        width: contentWidth > this.options.responsiveMinWidth ? contentWidth : null,
                        minWidth: contentWidth < parseInt(this.content.css('minWidth')) ? 0 : null
                    });
                    this._setTitleWidth();
                }
                jBoxDimensions = this._exposeDimensions();

            }.bind(this);
            options.responsiveWidth && adjustResponsiveWidth();

            // After adjusting width, check if jBox will be flipped for y
            options.responsiveWidth && !flip.y && (jBoxOutsidePosition.y && jBoxDimensions.y > availableSpace[jBoxOutsidePosition.y] && availableSpace[this._getOpp(jBoxOutsidePosition.y)] > availableSpace[jBoxOutsidePosition.y]) && (jBoxOutsidePosition.y = this._getOpp(jBoxOutsidePosition.y)) && (flip.y = true);

            // Adjust width and height according to default outside position
            var adjustResponsiveHeight = function () {
                if (options.responsiveHeight && jBoxDimensions.y > availableSpace[jBoxOutsidePosition.y || 'y']) {

                    // Expose wrapper to get correct title height
                    var exposeTitleFooterHeight = function () {
                        if (!this.titleContainer && !this.footer) return 0;
                        if (this.wrapper.css('display') == 'none') {
                            this.wrapper.css('display', 'block');
                            var height = (this.titleContainer ? this.titleContainer.outerHeight() : 0) + (this.footer ? this.footer.outerHeight() : 0);
                            this.wrapper.css('display', 'none');
                        } else {
                            var height = (this.titleContainer ? this.titleContainer.outerHeight() : 0) + (this.footer ? this.footer.outerHeight() : 0);
                        }
                        return height || 0;
                    }.bind(this);

                    var contentHeight = availableSpace[jBoxOutsidePosition.y || 'y'] - (this.pointer && outside && options.outside == 'y' ? this.pointer.dimensions.y : 0) - exposeTitleFooterHeight() - parseInt(this.container.css('border-top-width')) - parseInt(this.container.css('border-bottom-width'));
                    this.content.css({height: contentHeight > this.options.responsiveMinHeight ? contentHeight : null});
                    this._setTitleWidth();
                }
                jBoxDimensions = this._exposeDimensions();

            }.bind(this);
            options.responsiveHeight && adjustResponsiveHeight();

            // After adjusting height, check if jBox will be flipped for x
            options.responsiveHeight && !flip.x && (jBoxOutsidePosition.x && jBoxDimensions.x > availableSpace[jBoxOutsidePosition.x] && availableSpace[this._getOpp(jBoxOutsidePosition.x)] > availableSpace[jBoxOutsidePosition.x]) && (jBoxOutsidePosition.x = this._getOpp(jBoxOutsidePosition.x)) && (flip.x = true);

            // Adjust width and height if jBox will be flipped
            if (options.adjustPosition && options.adjustPosition != 'move') {
                flip.x && adjustResponsiveWidth();
                flip.y && adjustResponsiveHeight();
            }
        }

        // Store new positioning vars in local var
        var pos = {};

        // Calculate positions
        var setPosition = function (p) {
            // Set number positions
            if (jQuery.type(options.position[p]) == 'number') {
                pos[options.attributes[p]] = options.position[p];
                return;
            }

            // We have a target, so use 'left' or 'top' as attributes
            var a = options.attributes[p] = (p == 'x' ? 'left' : 'top');

            // Start at target position
            pos[a] = targetDimensions[a];

            // Set centered position
            if (options.position[p] == 'center') {
                pos[a] += Math.ceil((targetDimensions[p] - jBoxDimensions[p]) / 2);

                // If the target is the window, adjust centered position depending on adjustDistance
                (this.target != 'mouse' && this.target[0] && this.target[0] == jQuery(window)[0]) && (pos[a] += (options.adjustDistance[a] - options.adjustDistance[this._getOpp(a)]) * 0.5);
                return;
            }

            // Move inside
            (a != options.position[p]) && (pos[a] += targetDimensions[p] - jBoxDimensions[p]);

            // Move outside
            (options.outside == p || options.outside == 'xy') && (pos[a] += jBoxDimensions[p] * (a != options.position[p] ? 1 : -1));

        }.bind(this);

        // Set position including offset
        setPosition('x');
        setPosition('y');

        // Adjust position depending on pointer align
        if (this.pointer && options.pointTo == 'target' && jQuery.type(options.position.x) != 'number' && jQuery.type(options.position.y) != 'number') {

            var adjustWrapper = 0;

            // Where is the pointer aligned? Add or substract accordingly
            switch (this.pointer.align) {
                case 'center':
                    if (options.position[this._getOpp(options.outside)] != 'center') {
                        adjustWrapper += (jBoxDimensions[this._getOpp(options.outside)] / 2);
                    }
                    break;
                default:
                    switch (options.position[this._getOpp(options.outside)]) {
                        case 'center':
                            adjustWrapper += ((jBoxDimensions[this._getOpp(options.outside)] / 2) - (this.pointer.dimensions[this._getOpp(options.outside)] / 2)) * (this.pointer.align == this._getTL(this.pointer.align) ? 1 : -1);
                            break;
                        default:
                            adjustWrapper += (this.pointer.align != options.position[this._getOpp(options.outside)]) ?

                                // If pointer align is different to position align
                                (this.dimensions[this._getOpp(options.outside)] * (jQuery.inArray(this.pointer.align, ['top', 'left']) !== -1 ? 1 : -1)) + ((this.pointer.dimensions[this._getOpp(options.outside)] / 2) * (jQuery.inArray(this.pointer.align, ['top', 'left']) !== -1 ? -1 : 1)) :

                                // If pointer align is same as position align
                                (this.pointer.dimensions[this._getOpp(options.outside)] / 2) * (jQuery.inArray(this.pointer.align, ['top', 'left']) !== -1 ? 1 : -1);
                            break;
                    }
                    break;
            }

            adjustWrapper *= (options.position[this._getOpp(options.outside)] == this.pointer.alignAttribute ? -1 : 1);
            adjustWrapper += this.pointer.offset * (this.pointer.align == this._getOpp(this._getTL(this.pointer.align)) ? 1 : -1);

            pos[this._getTL(this._getOpp(this.pointer.xy))] += adjustWrapper;
        }

        // Add final offset
        pos[options.attributes.x] += options.offset.x;
        pos[options.attributes.y] += options.offset.y;

        // Set CSS
        this.wrapper.css(pos);

        // Adjust position
        if (options.adjustPosition) {

            // Reset cached pointer position
            if (this.positionAdjusted) {
                this.pointer && this.wrapper.css('padding', 0).css('padding-' + this._getOpp(this.outside), this.pointer.dimensions[this._getXY(this.outside)]).removeClass('jBox-pointerPosition-' + this._getOpp(this.pointer.position)).addClass('jBox-pointerPosition-' + this.pointer.position);
                this.pointer && this.pointer.element.attr('class', 'jBox-pointer jBox-pointer-' + this._getOpp(this.outside)).css(this.pointer.margin);
                this.positionAdjusted = false;
                this.flipped = false;
            }

            // Find out where the jBox is out of view area
            var outYT = (windowDimensions.top > pos.top - (options.adjustDistance.top || 0)),
                outXR = (windowDimensions.right < pos.left + jBoxDimensions.x + (options.adjustDistance.right || 0)),
                outYB = (windowDimensions.bottom < pos.top + jBoxDimensions.y + (options.adjustDistance.bottom || 0)),
                outXL = (windowDimensions.left > pos.left - (options.adjustDistance.left || 0)),
                outX = outXL ? 'left' : (outXR ? 'right' : null),
                outY = outYT ? 'top' : (outYB ? 'bottom' : null),
                out = outX || outY;

            // Only continue if jBox is out of view area
            if (out) {

                // Function to flip position
                var flipJBox = function (xy) {
                    this.wrapper.css(this._getTL(xy), pos[this._getTL(xy)] + ((jBoxDimensions[this._getXY(xy)] + (options.offset[this._getXY(xy)] * (xy == 'top' || xy == 'left' ? -2 : 2)) + targetDimensions[this._getXY(xy)]) * (xy == 'top' || xy == 'left' ? 1 : -1)));
                    this.pointer && this.wrapper.removeClass('jBox-pointerPosition-' + this.pointer.position).addClass('jBox-pointerPosition-' + this._getOpp(this.pointer.position)).css('padding', 0).css('padding-' + xy, this.pointer.dimensions[this._getXY(xy)]);
                    this.pointer && this.pointer.element.attr('class', 'jBox-pointer jBox-pointer-' + xy);
                    this.positionAdjusted = true;
                    this.flipped = true;
                }.bind(this);

                // Flip jBox
                flip.x && flipJBox(this.options.position.x);
                flip.y && flipJBox(this.options.position.y);

                // Move jBox (only possible with pointer)
                var outMove = (this._getXY(this.outside) == 'x') ? outY : outX;

                if (this.pointer && options.pointTo == 'target' && options.adjustPosition != 'flip' && this._getXY(outMove) == this._getOpp(this._getXY(this.outside))) {

                    // Get the maximum space we have availible to adjust
                    if (this.pointer.align == 'center') {
                        var spaceAvail = (jBoxDimensions[this._getXY(outMove)] / 2) - (this.pointer.dimensions[this._getOpp(this.pointer.xy)] / 2) - (parseInt(this.pointer.element.css('margin-' + this.pointer.alignAttribute)) * (outMove != this._getTL(outMove) ? -1 : 1));
                    } else {
                        var spaceAvail = (outMove == this.pointer.alignAttribute) ?
                            parseInt(this.pointer.element.css('margin-' + this.pointer.alignAttribute)) :
                            jBoxDimensions[this._getXY(outMove)] - parseInt(this.pointer.element.css('margin-' + this.pointer.alignAttribute)) - this.pointer.dimensions[this._getXY(outMove)];
                    }

                    // Get the overlapping space
                    spaceDiff = (outMove == this._getTL(outMove)) ?
                        windowDimensions[this._getTL(outMove)] - pos[this._getTL(outMove)] + options.adjustDistance[outMove] :
                        (windowDimensions[this._getOpp(this._getTL(outMove))] - pos[this._getTL(outMove)] - options.adjustDistance[outMove] - jBoxDimensions[this._getXY(outMove)]) * -1;

                    // Add overlapping space on left or top window edge
                    if (outMove == this._getOpp(this._getTL(outMove)) && pos[this._getTL(outMove)] - spaceDiff < windowDimensions[this._getTL(outMove)] + options.adjustDistance[this._getTL(outMove)]) {
                        spaceDiff -= windowDimensions[this._getTL(outMove)] + options.adjustDistance[this._getTL(outMove)] - (this.pos[this._getTL(outMove)] - spaceDiff);
                    }

                    // Only adjust the maximum availible
                    spaceDiff = Math.min(spaceDiff, spaceAvail);

                    // Move jBox
                    if (spaceDiff <= spaceAvail && spaceDiff > 0) {
                        this.pointer.element.css('margin-' + this.pointer.alignAttribute, parseInt(this.pointer.element.css('margin-' + this.pointer.alignAttribute)) - (spaceDiff * (outMove != this.pointer.alignAttribute ? -1 : 1)));
                        this.wrapper.css(this._getTL(outMove), pos[this._getTL(outMove)] + (spaceDiff * (outMove != this._getTL(outMove) ? -1 : 1)));
                        this.positionAdjusted = true;
                    }
                }
            }
        }

        // Fire onPosition event
        this._fireEvent('onPosition');

        return this;
    };


    // Open jBox

    jBox.prototype.open = function (options) {
        // Create blank options if none passed
        !options && (options = {});

        // Abort if jBox was destroyed
        if (this.isDestroyed) return false;

        // Construct jBox if not already constructed
        !this.wrapper && this._create();

        // Add css to header if not added already
        !this._styles && (this._styles = jQuery('<style/>').append(this._animationCSS).appendTo(jQuery('head')));

        // Abort any opening or closing timer
        this.timer && clearTimeout(this.timer);

        // Block body click for 10ms, so jBox can open on attached elements while closeOnClick = 'body'
        this._blockBodyClick();

        // Block opening
        if (this.isDisabled) return this;

        // Opening function
        var open = function () {

            // Set title from source element
            this.source && this.options.getTitle && (this.source.attr(this.options.getTitle) && this.setTitle(this.source.attr(this.options.getTitle)), true);

            // Set content from source element
            this.source && this.options.getContent && (this.source.data('jBox-getContent') ? this.setContent(this.source.data('jBox-getContent'), true) : (this.source.attr(this.options.getContent) ? this.setContent(this.source.attr(this.options.getContent), true) : (this.options.getContent == 'html' ? this.setContent(this.source.html(), true) : null)));

            // Fire onOpen event
            this._fireEvent('onOpen');

            // Get content from ajax
            if ((this.options.ajax && (this.options.ajax.url || (this.source && this.source.attr(this.options.ajax.getURL))) && (!this.ajaxLoaded || this.options.ajax.reload)) || (options.ajax && (options.ajax.url || options.ajax.data))) {
                // Send the content from stored data if there is any, otherwise load new data
                (this.options.ajax.reload != 'strict' && this.source && this.source.data('jBox-ajax-data') && !(options.ajax && (options.ajax.url || options.ajax.data))) ? this.setContent(this.source.data('jBox-ajax-data')) : this.ajax((options.ajax || null), true);
            }

            // Set position
            (!this.positionedOnOpen || this.options.repositionOnOpen) && this.position(options) && (this.positionedOnOpen = true);

            // Abort closing
            this.isClosing && this._abortAnimation();

            // Open functions to call when jBox is closed
            if (!this.isOpen) {

                // jBox is open now
                this.isOpen = true;

                // Automatically close jBox after some time
                this.options.autoClose && (this.options.delayClose = this.options.autoClose) && this.close();

                // Attach events
                this._attachEvents();

                // Block scrolling
                this.options.blockScroll && jQuery('body').addClass('jBox-blockScroll-' + this.id);

                // Show overlay
                this.options.overlay && this._showOverlay();

                // Only animate if jBox is completely closed
                this.options.animation && !this.isClosing && this._animate('open');

                // Play audio file
                this.options.audio && this.options.audio.open && this.audio(this.options.audio.open, this.options.volume.open);

                // Fading animation or show immediately
                if (this.options.fade) {
                    this.wrapper.stop().animate({opacity: 1}, {
                        queue: false,
                        duration: this.options.fade,
                        start: function () {
                            this.isOpening = true;
                            this.wrapper.css({display: 'block'});
                        }.bind(this),
                        always: function () {
                            this.isOpening = false;

                            // Delay positioning for ajax to prevent positioning during animation
                            setTimeout(function () {
                                this.positionOnFadeComplete && this.position() && (this.positionOnFadeComplete = false);
                            }.bind(this), 10);
                        }.bind(this)
                    });
                } else {
                    this.wrapper.css({display: 'block', opacity: 1});
                    this.positionOnFadeComplete && this.position() && (this.positionOnFadeComplete = false);
                }
            }
        }.bind(this);

        // Open jBox
        this.options.delayOpen && !this.isOpen && !this.isClosing && !options.ignoreDelay ? (this.timer = setTimeout(open, this.options.delayOpen)) : open();

        return this;
    };


    // Close jBox

    jBox.prototype.close = function (options) {
        // Create blank options if none passed
        options || (options = {});

        // Abort if jBox was destroyed or is currently closing
        if (this.isDestroyed || this.isClosing) return false;

        // Abort opening
        this.timer && clearTimeout(this.timer);

        // Block body click for 10ms, so jBox can open on attached elements while closeOnClock = 'body' is true
        this._blockBodyClick();

        // Block closing
        if (this.isDisabled) return this;

        // Close function
        var close = function () {

            // Fire onClose event
            this._fireEvent('onClose');

            // Only close if jBox is open
            if (this.isOpen) {

                // jBox is not open anymore
                this.isOpen = false;

                // Detach events
                this._detachEvents();

                // Unblock scrolling
                this.options.blockScroll && jQuery('body').removeClass('jBox-blockScroll-' + this.id);

                // Hide overlay
                this.options.overlay && this._hideOverlay();

                // Only animate if jBox is compleately closed
                this.options.animation && !this.isOpening && this._animate('close');

                // Play audio file
                this.options.audio && this.options.audio.close && this.audio(this.options.audio.close, this.options.volume.close);

                // Fading animation or show immediately
                if (this.options.fade) {
                    this.wrapper.stop().animate({opacity: 0}, {
                        queue: false,
                        duration: this.options.fade,
                        start: function () {
                            this.isClosing = true;
                        }.bind(this),
                        complete: function () {
                            this.wrapper.css({display: 'none'});
                            this._fireEvent('onCloseComplete');
                        }.bind(this),
                        always: function () {
                            this.isClosing = false;
                        }.bind(this)
                    });
                } else {
                    this.wrapper.css({display: 'none', opacity: 0});
                    this._fireEvent('onCloseComplete');
                }
            }
        }.bind(this);

        // Close jBox
        options.ignoreDelay ? close() : (this.timer = setTimeout(close, Math.max(this.options.delayClose, 10)));

        return this;
    };


    // Open or close jBox

    jBox.prototype.toggle = function (options) {
        this[this.isOpen ? 'close' : 'open'](options);
        return this;
    };


    // Block opening and closing

    jBox.prototype.disable = function () {
        this.isDisabled = true;
        return this;
    };


    // Unblock opening and closing

    jBox.prototype.enable = function () {
        this.isDisabled = false;
        return this;
    };


    // Hide jBox

    jBox.prototype.hide = function () {
        this.disable();
        this.wrapper && this.wrapper.css({display: 'none'});
        return this;
    };


    // Show jBox

    jBox.prototype.show = function () {
        this.enable();
        this.wrapper && this.wrapper.css({display: 'block'});
        return this;
    };


    // Get content from ajax

    jBox.prototype.ajax = function (options, opening) {
        options || (options = {});

        // Add data or url from source element if none set in options
        jQuery.each([['getData', 'data'], ['getURL', 'url']], function (index, item) {
            (this.options.ajax[item[0]] && !options[item[1]] && this.source && this.source.attr(this.options.ajax[item[0]]) != undefined) && (options[item[1]] = this.source.attr(this.options.ajax[item[0]]) || '');
        }.bind(this));

        // Clone the system options
        var sysOptions = jQuery.extend(true, {}, this.options.ajax);

        // Abort running ajax call
        this.ajaxRequest && this.ajaxRequest.abort();

        // Extract events
        var beforeSend = options.beforeSend || sysOptions.beforeSend || function () {
        };
        var complete = options.complete || sysOptions.complete || function () {
        };
        var success = options.success || sysOptions.success || function () {
        };
        var error = options.error || sysOptions.error || function () {
        };

        // Merge options
        var userOptions = jQuery.extend(true, sysOptions, options);

        // Set new beforeSend event
        userOptions.beforeSend = function () {
            // jBox is loading
            this.wrapper.addClass('jBox-loading');

            // Add loading spinner
            userOptions.spinner && (this.spinnerDelay = setTimeout(function () {
                // If there is a dela
                this.wrapper.addClass('jBox-loading-spinner');

                // Reposition jBox
                // TODO: Only reposition if dimensions change
                userOptions.spinnerReposition && (opening ? (this.positionOnFadeComplete = true) : this.position());

                // Add spinner to container
                this.spinner = jQuery(userOptions.spinner !== true ? userOptions.spinner : '<div class="jBox-spinner"></div>').appendTo(this.container);

                // Fix spinners position if there is a title
                this.titleContainer && this.spinner.css('position') == 'absolute' && this.spinner.css({transform: 'translateY(' + (this.titleContainer.outerHeight() * 0.5) + 'px)'});

            }.bind(this), (this.content.html() == '' ? 0 : (userOptions.spinnerDelay || 0))));

            // Fire users beforeSend event
            (beforeSend.bind(this))();

        }.bind(this);

        // Set up new complete event
        userOptions.complete = function (response) {
            // Abort spinner timeout
            this.spinnerDelay && clearTimeout(this.spinnerDelay);

            // jBox finished loading
            this.wrapper.removeClass('jBox-loading jBox-loading-spinner jBox-loading-spinner-delay');

            // Remove spinner
            this.spinner && this.spinner.length && this.spinner.remove() && userOptions.spinnerReposition && (opening ? (this.positionOnFadeComplete = true) : this.position());

            // Store that ajax loading finished
            this.ajaxLoaded = true;

            // Fire users complete event
            (complete.bind(this))(response);

        }.bind(this);

        // Set up new success event
        userOptions.success = function (response) {
            // Set content
            userOptions.setContent && this.setContent(response, true) && (opening ? (this.positionOnFadeComplete = true) : this.position());

            // Store content in source element
            userOptions.setContent && this.source && this.source.data('jBox-ajax-data', response);

            // Fire users success event
            (success.bind(this))(response);

        }.bind(this);

        // Add error event
        userOptions.error = function (response) {
            (error.bind(this))(response);
        }.bind(this);

        // Send new ajax request
        this.ajaxRequest = jQuery.ajax(userOptions);

        return this;
    };


    // Play an audio file

    jBox.prototype.audio = function (url, volume) {
        // URL is required
        if (!url) return this;

        // Create intern audio object if it wasn't created already
        !jBox._audio && (jBox._audio = {});

        // Create an audio element specific to this audio file if it doesn't exist already
        if (!jBox._audio[url]) {
            var audio = jQuery('<audio/>');
            jQuery('<source/>', {src: url + '.mp3'}).appendTo(audio);
            jQuery('<source/>', {src: url + '.ogg'}).appendTo(audio);
            jBox._audio[url] = audio[0];
        }

        // Set volume
        jBox._audio[url].volume = Math.min(((volume != undefined ? volume : 100) / 100), 1);

        // Try to pause current audio
        try {
            jBox._audio[url].pause();
            jBox._audio[url].currentTime = 0;
        } catch (e) {
        }

        // Play audio
        jBox._audio[url].play();

        return this;
    };


    // Apply custom animations to jBox

    jBox._animationSpeeds = {
        'tada': 1000,
        'tadaSmall': 1000,
        'flash': 500,
        'shake': 400,
        'pulseUp': 250,
        'pulseDown': 250,
        'popIn': 250,
        'popOut': 250,
        'fadeIn': 200,
        'fadeOut': 200,
        'slideUp': 400,
        'slideRight': 400,
        'slideLeft': 400,
        'slideDown': 400
    };

    jBox.prototype.animate = function (animation, options) {
        // Options are required
        !options && (options = {});

        // Timout needs to be an object
        !this.animationTimeout && (this.animationTimeout = {});

        // Use jBox wrapper by default
        !options.element && (options.element = this.wrapper);

        // Give the element an unique id
        !options.element.data('jBox-animating-id') && options.element.data('jBox-animating-id', jBox._getUniqueElementID());

        // Abort if element is animating
        if (options.element.data('jBox-animating')) {
            options.element.removeClass(options.element.data('jBox-animating')).data('jBox-animating', null);
            this.animationTimeout[options.element.data('jBox-animating-id')] && clearTimeout(this.animationTimeout[options.element.data('jBox-animating-id')]);
        }

        // Animate the element
        options.element.addClass('jBox-animated-' + animation).data('jBox-animating', 'jBox-animated-' + animation);
        this.animationTimeout[options.element.data('jBox-animating-id')] = setTimeout((function () {
            options.element.removeClass(options.element.data('jBox-animating')).data('jBox-animating', null);
            options.complete && options.complete();
        }), jBox._animationSpeeds[animation]);
    };


    // Destroy jBox and remove it from DOM

    jBox.prototype.destroy = function () {
        // Detach from attached elements
        this.detach();

        // If jBox is open, close without delay
        this.isOpen && this.close({ignoreDelay: true});

        // Remove wrapper
        this.wrapper && this.wrapper.remove();

        // Remove overlay
        this.overlay && this.overlay.remove();

        // Remove styles
        this._styles && this._styles.remove();

        // Tell the jBox instance it is destroyed
        this.isDestroyed = true;

        return this;
    };


    // Get a unique ID for jBoxes

    jBox._getUniqueID = (function () {
        var i = 1;
        return function () {
            return i++;
        };
    }());


    // Get a unique ID for animating elements

    jBox._getUniqueElementID = (function () {
        var i = 1;
        return function () {
            return i++;
        };
    }());


    // Function to create jBox plugins

    jBox._pluginOptions = {};
    jBox.plugin = function (type, options) {
        jBox._pluginOptions[type] = options;
    };


    // Make jBox usable with jQuery selectors

    jQuery.fn.jBox = function (type, options) {
        // Variables type and object are required
        !type && (type = {});
        !options && (options = {});

        // Return a new instance of jBox with the selector as attached element
        return new jBox(type, jQuery.extend(options, {
            attach: this
        }));
    };

    return jBox;
}));